/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.novoda.noplayer;

import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Join;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.graphics.RectF;
import android.text.Layout.Alignment;
import android.text.SpannableStringBuilder;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.RelativeSizeSpan;
import android.util.DisplayMetrics;
import android.util.Log;

import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.util.Util;
import com.novoda.noplayer.model.NoPlayerCue;

// Adopted code, could use some refactoring but it's a complex job
@SuppressWarnings({"PMD.GodClass", "PMD.CyclomaticComplexity", "PMD.StdCyclomaticComplexity", "PMD.ModifiedCyclomaticComplexity"})
final class SubtitlePainter {

    private static final String TAG = "SubtitlePainter";

    private static final float INNER_PADDING_RATIO = 0.125f;
    private static final float ROUNDING_HALF_PIXEL = 0.5f;
    private static final float TWO_DP = 2f;
    private static final double FLOAT_COMPARISON_EPSILON = .0000001;

    private final RectF lineBounds = new RectF();

    // Styled dimensions.
    private final float cornerRadius;
    private final float outlineWidth;
    private final float shadowRadius;
    private final float shadowOffset;
    private final float spacingMult;
    private final float spacingAdd;

    private final TextPaint textPaint;
    private final Paint paint;

    // Previous input variables.
    private CharSequence cueText;
    private Alignment cueTextAlignment;
    private Bitmap cueBitmap;
    private float cueLine;
    @Cue.LineType
    private int cueLineType;
    @Cue.AnchorType
    private int cueLineAnchor;
    private float cuePosition;
    @Cue.AnchorType
    private int cuePositionAnchor;
    private float cueSize;
    private float cueBitmapHeight;
    private boolean applyEmbeddedStyles;
    private boolean applyEmbeddedFontSizes;
    private int foregroundColor;
    private int backgroundColor;
    private int windowColor;
    private int edgeColor;
    @CaptionStyleCompat.EdgeType
    private int edgeType;
    private float textSizePx;
    private float bottomPaddingFraction;
    private int parentLeft;
    private int parentTop;
    private int parentRight;
    private int parentBottom;

    // Derived drawing variables.
    private StaticLayout textLayout;
    private int textLeft;
    private int textTop;
    private int textPaddingX;
    private Rect bitmapRect;

    @SuppressWarnings("ResourceType")        // We're hacking `spacingMult = styledAttributes.getFloat`
    SubtitlePainter(Context context) {
        int[] viewAttr = {android.R.attr.lineSpacingExtra, android.R.attr.lineSpacingMultiplier};
        TypedArray styledAttributes = context.obtainStyledAttributes(null, viewAttr, 0, 0);
        spacingAdd = styledAttributes.getDimensionPixelSize(0, 0);
        spacingMult = styledAttributes.getFloat(1, 1);
        styledAttributes.recycle();

        Resources resources = context.getResources();
        DisplayMetrics displayMetrics = resources.getDisplayMetrics();
        int twoDpInPx = Math.round((TWO_DP * displayMetrics.densityDpi) / DisplayMetrics.DENSITY_DEFAULT);
        cornerRadius = twoDpInPx;
        outlineWidth = twoDpInPx;
        shadowRadius = twoDpInPx;
        shadowOffset = twoDpInPx;

        textPaint = new TextPaint();
        textPaint.setAntiAlias(true);
        textPaint.setSubpixelText(true);

        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStyle(Style.FILL);
    }

    @SuppressWarnings({"checkstyle:ParameterNumber", "PMD.ExcessiveParameterList"}) // TODO group parameters into classes
    void draw(NoPlayerCue cue,
              boolean applyEmbeddedStyles,
              boolean applyEmbeddedFontSizes,
              float textSizePx,
              float bottomPaddingFraction,
              Canvas canvas,
              int cueBoxLeft,
              int cueBoxTop,
              int cueBoxRight,
              int cueBoxBottom) {
        boolean isTextCue = cue.bitmap() == null;
        int windowColor = Color.BLACK;
        if (isTextCue) {
            if (TextUtils.isEmpty(cue.text())) {
                // Nothing to draw.
                return;
            }
            windowColor = (cue.windowColorSet() && applyEmbeddedStyles)
                    ? cue.windowColor() : Color.TRANSPARENT;
        }
        if (nothingHasChanged(
                cue,
                applyEmbeddedStyles,
                applyEmbeddedFontSizes,
                textSizePx,
                bottomPaddingFraction,
                cueBoxLeft,
                cueBoxTop,
                cueBoxRight,
                cueBoxBottom,
                windowColor)) {
            // We can use the cached layout.
            drawLayout(canvas, isTextCue);
            return;
        }

        this.cueText = cue.text();
        this.cueTextAlignment = cue.textAlignment();
        this.cueBitmap = cue.bitmap();
        this.cueLine = cue.line();
        this.cueLineType = cue.lineType();
        this.cueLineAnchor = cue.lineAnchor();
        this.cuePosition = cue.position();
        this.cuePositionAnchor = cue.positionAnchor();
        this.cueSize = cue.size();
        this.cueBitmapHeight = cue.bitmapHeight();
        this.applyEmbeddedStyles = applyEmbeddedStyles;
        this.applyEmbeddedFontSizes = applyEmbeddedFontSizes;
        this.foregroundColor = Color.WHITE;
        this.backgroundColor = Color.BLACK;
        this.windowColor = windowColor;
        this.edgeType = 0;
        this.edgeColor = Color.WHITE;
        textPaint.setTypeface(null);
        this.textSizePx = textSizePx;
        this.bottomPaddingFraction = bottomPaddingFraction;
        this.parentLeft = cueBoxLeft;
        this.parentTop = cueBoxTop;
        this.parentRight = cueBoxRight;
        this.parentBottom = cueBoxBottom;

        if (isTextCue) {
            setupTextLayout();
        } else {
            setupBitmapLayout();
        }
        drawLayout(canvas, isTextCue);
    }

    @SuppressWarnings({"checkstyle:ParameterNumber", "PMD.ExcessiveParameterList"})     // TODO group parameters into classes
    private boolean nothingHasChanged(NoPlayerCue cue,
                                      boolean applyEmbeddedStyles,
                                      boolean applyEmbeddedFontSizes,
                                      float textSizePx,
                                      float bottomPaddingFraction,
                                      int cueBoxLeft,
                                      int cueBoxTop,
                                      int cueBoxRight,
                                      int cueBoxBottom,
                                      int windowColor) {
        return areCharSequencesEqual(cueText, cue.text())
                && Util.areEqual(cueTextAlignment, cue.textAlignment())
                && cueBitmap == cue.bitmap()
                && cueLine == cue.line()
                && cueLineType == cue.lineType()
                && Util.areEqual(cueLineAnchor, cue.lineAnchor())
                && cuePosition == cue.position()
                && Util.areEqual(cuePositionAnchor, cue.positionAnchor())
                && cueSize == cue.size()
                && cueBitmapHeight == cue.bitmapHeight()
                && this.applyEmbeddedStyles == applyEmbeddedStyles
                && this.applyEmbeddedFontSizes == applyEmbeddedFontSizes
                && foregroundColor == Color.WHITE
                && backgroundColor == Color.BLACK
                && this.windowColor == windowColor
                && edgeType == 0
                && edgeColor == Color.WHITE
                && Util.areEqual(textPaint.getTypeface(), null)
                && this.textSizePx == textSizePx
                && this.bottomPaddingFraction == bottomPaddingFraction
                && parentLeft == cueBoxLeft
                && parentTop == cueBoxTop
                && parentRight == cueBoxRight
                && parentBottom == cueBoxBottom;
    }

    @SuppressWarnings({"PMD.ExcessiveMethodLength", "PMD.NPathComplexity" })  // TODO break this method up
    private void setupTextLayout() {
        int parentWidth = parentRight - parentLeft;

        textPaint.setTextSize(textSizePx);
        int textPaddingX = (int) (textSizePx * INNER_PADDING_RATIO + ROUNDING_HALF_PIXEL);

        int availableWidth = parentWidth - textPaddingX * 2;
        if (isCueDimensionSet(cueSize)) {
            availableWidth *= cueSize;
        }
        if (availableWidth <= 0) {
            Log.w(TAG, "Skipped drawing subtitle cue (insufficient space)");
            return;
        }

        // Remove embedded styling or font size if requested.
        CharSequence cueText;
        if (applyEmbeddedFontSizes && applyEmbeddedStyles) {
            cueText = this.cueText;
        } else if (!applyEmbeddedStyles) {
            cueText = this.cueText.toString(); // Equivalent to erasing all spans.
        } else {
            SpannableStringBuilder newCueText = new SpannableStringBuilder(this.cueText);
            int cueLength = newCueText.length();
            AbsoluteSizeSpan[] absSpans = newCueText.getSpans(0, cueLength, AbsoluteSizeSpan.class);
            RelativeSizeSpan[] relSpans = newCueText.getSpans(0, cueLength, RelativeSizeSpan.class);
            for (AbsoluteSizeSpan absSpan : absSpans) {
                newCueText.removeSpan(absSpan);
            }
            for (RelativeSizeSpan relSpan : relSpans) {
                newCueText.removeSpan(relSpan);
            }
            cueText = newCueText;
        }

        Alignment textAlignment = cueTextAlignment == null ? Alignment.ALIGN_CENTER : cueTextAlignment;
        textLayout = new StaticLayout(cueText, textPaint, availableWidth, textAlignment, spacingMult,
                spacingAdd, true);
        int textWidth = 0;
        int lineCount = textLayout.getLineCount();
        for (int i = 0; i < lineCount; i++) {
            textWidth = Math.max((int) Math.ceil(textLayout.getLineWidth(i)), textWidth);
        }
        if (isCueDimensionSet(cueSize) && textWidth < availableWidth) {
            textWidth = availableWidth;
        }
        textWidth += textPaddingX * 2;

        int textLeft;
        int textRight;
        if (isCueDimensionSet(cuePosition)) {
            int anchorPosition = Math.round(parentWidth * cuePosition) + parentLeft;
            textLeft = cuePositionAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textWidth
                    : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textWidth) / 2
                    : anchorPosition;
            textLeft = Math.max(textLeft, parentLeft);
            textRight = Math.min(textLeft + textWidth, parentRight);
        } else {
            textLeft = (parentWidth - textWidth) / 2;
            textRight = textLeft + textWidth;
        }

        textWidth = textRight - textLeft;
        if (textWidth <= 0) {
            Log.w(TAG, "Skipped drawing subtitle cue (invalid horizontal positioning)");
            return;
        }

        int parentHeight = parentBottom - parentTop;
        int textHeight = textLayout.getHeight();

        int textTop;
        if (isCueDimensionSet(cueLine)) {
            int anchorPosition;
            if (cueLineType == Cue.LINE_TYPE_FRACTION) {
                anchorPosition = Math.round(parentHeight * cueLine) + parentTop;
            } else {
                // cueLineType == Cue.LINE_TYPE_NUMBER
                int firstLineHeight = textLayout.getLineBottom(0) - textLayout.getLineTop(0);
                if (cueLine >= 0) {
                    anchorPosition = Math.round(cueLine * firstLineHeight) + parentTop;
                } else {
                    anchorPosition = Math.round((cueLine + 1) * firstLineHeight) + parentBottom;
                }
            }
            textTop = cueLineAnchor == Cue.ANCHOR_TYPE_END ? anchorPosition - textHeight
                    : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorPosition * 2 - textHeight) / 2
                    : anchorPosition;
            if (textTop + textHeight > parentBottom) {
                textTop = parentBottom - textHeight;
            } else if (textTop < parentTop) {
                textTop = parentTop;
            }
        } else {
            textTop = parentBottom - textHeight - (int) (parentHeight * bottomPaddingFraction);
        }

        // Update the derived drawing variables.
        this.textLayout = new StaticLayout(cueText, textPaint, textWidth, textAlignment, spacingMult,
                spacingAdd, true);
        this.textLeft = textLeft;
        this.textTop = textTop;
        this.textPaddingX = textPaddingX;
    }

    @SuppressWarnings("PMD.NPathComplexity")  // TODO break this method up
    private void setupBitmapLayout() {
        int parentWidth = parentRight - parentLeft;
        int parentHeight = parentBottom - parentTop;
        float anchorX = parentLeft + (parentWidth * cuePosition);
        float anchorY = parentTop + (parentHeight * cueLine);
        int width = Math.round(parentWidth * cueSize);

        int height = isCueDimensionSet(cueBitmapHeight)
                ? Math.round(parentHeight * cueBitmapHeight)
                : Math.round(width * ((float) cueBitmap.getHeight() / cueBitmap.getWidth()));

        int x = Math.round(cueLineAnchor == Cue.ANCHOR_TYPE_END
                ? (anchorX - width)
                : cueLineAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorX - (width / 2f)) : anchorX);

        int y = Math.round(cuePositionAnchor == Cue.ANCHOR_TYPE_END
                ? (anchorY - height)
                : cuePositionAnchor == Cue.ANCHOR_TYPE_MIDDLE ? (anchorY - (height / 2f)) : anchorY);

        bitmapRect = new Rect(x, y, x + width, y + height);
    }

    private boolean isCueDimensionSet(float cueDimension) {
        return Math.abs(cueDimension - Cue.DIMEN_UNSET) > FLOAT_COMPARISON_EPSILON;
    }

    private void drawLayout(Canvas canvas, boolean isTextCue) {
        if (isTextCue) {
            drawTextLayout(canvas);
        } else {
            drawBitmapLayout(canvas);
        }
    }

    @SuppressWarnings("PMD.NPathComplexity")  // TODO break this method up
    private void drawTextLayout(Canvas canvas) {
        StaticLayout layout = textLayout;
        if (layout == null) {
            // Nothing to draw.
            return;
        }

        int saveCount = canvas.save();
        canvas.translate(textLeft, textTop);

        if (Color.alpha(windowColor) > 0) {
            paint.setColor(windowColor);
            canvas.drawRect(-textPaddingX, 0, layout.getWidth() + textPaddingX, layout.getHeight(),
                    paint);
        }

        if (Color.alpha(backgroundColor) > 0) {
            paint.setColor(backgroundColor);
            float previousBottom = layout.getLineTop(0);
            int lineCount = layout.getLineCount();
            for (int i = 0; i < lineCount; i++) {
                lineBounds.left = layout.getLineLeft(i) - textPaddingX;
                lineBounds.right = layout.getLineRight(i) + textPaddingX;
                lineBounds.top = previousBottom;
                lineBounds.bottom = layout.getLineBottom(i);
                previousBottom = lineBounds.bottom;
                canvas.drawRoundRect(lineBounds, cornerRadius, cornerRadius, paint);
            }
        }

        if (edgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) {
            textPaint.setStrokeJoin(Join.ROUND);
            textPaint.setStrokeWidth(outlineWidth);
            textPaint.setColor(edgeColor);
            textPaint.setStyle(Style.FILL_AND_STROKE);
            layout.draw(canvas);
        } else if (edgeType == CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW) {
            textPaint.setShadowLayer(shadowRadius, shadowOffset, shadowOffset, edgeColor);
        } else if (edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED
                || edgeType == CaptionStyleCompat.EDGE_TYPE_DEPRESSED) {
            boolean raised = edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED;
            int colorUp = raised ? Color.WHITE : edgeColor;
            int colorDown = raised ? edgeColor : Color.WHITE;
            float offset = shadowRadius / 2;
            textPaint.setColor(foregroundColor);
            textPaint.setStyle(Style.FILL);
            textPaint.setShadowLayer(shadowRadius, -offset, -offset, colorUp);
            layout.draw(canvas);
            textPaint.setShadowLayer(shadowRadius, offset, offset, colorDown);
        }

        textPaint.setColor(foregroundColor);
        textPaint.setStyle(Style.FILL);
        layout.draw(canvas);
        textPaint.setShadowLayer(0, 0, 0, 0);

        canvas.restoreToCount(saveCount);
    }

    private void drawBitmapLayout(Canvas canvas) {
        canvas.drawBitmap(cueBitmap, null, bitmapRect, null);
    }

    /**
     * This method is used instead of {@link TextUtils#equals(CharSequence, CharSequence)} because the
     * latter only checks the text of each sequence, and does not check for equality of styling that
     * may be embedded within the {@link CharSequence}s.
     */
    @SuppressWarnings("PMD.CompareObjectsWithEquals")   // We do, but we first try to shortcut by comparing references
    private static boolean areCharSequencesEqual(CharSequence first, CharSequence second) {
        // Some CharSequence implementations don't perform a cheap referential equality check in their
        // equals methods, so we perform one explicitly here.
        return first == second || (first != null && first.equals(second));
    }
}
